/*
* Copyright 2010 Pavel Kozhin 
*
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed  under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License.
*
* Author: Pavel Kozhin.
*/

package org.bookmarksframework.manager
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	import mx.core.ClassFactory;
	import mx.core.IFactory;
	import mx.utils.UIDUtil;
	
	import org.bookmarksframework.BookmarksConfig;
	import org.bookmarksframework.core.CommitInfo;
	import org.bookmarksframework.core.Committer;
	import org.bookmarksframework.core.FacileHierarchy;
	import org.bookmarksframework.core.FragmentController;
	import org.bookmarksframework.core.ICommitter;
	import org.bookmarksframework.core.IFragment;
	import org.bookmarksframework.core.IFragmentController;
	import org.bookmarksframework.core.IStorage;
	import org.bookmarksframework.core.Map;
	import org.bookmarksframework.core.Path;
	import org.bookmarksframework.core.Storage;
	import org.bookmarksframework.core.browser.BrowserAdapterEvents;
	import org.bookmarksframework.core.browser.IBrowserAdapter;
	import org.bookmarksframework.event.BookmarksEvent;
	import org.bookmarksframework.event.FragmentChangeEvent;
	import org.bookmarksframework.event.FragmentEvent;
	import org.bookmarksframework.rule.RuleParser;
	import org.bookmarksframework.util.CallLater;
	
	[Event(name="commit", type="org.bookmarksframework.event.FragmentCommitEvent")]
	
	[Event(name="change", type="org.bookmarksframework.event.FragmentChangeEvent")]
	
	[Event(name="commitComplete", type="org.bookmarksframework.event.BookmarksEvent")]
	
	[Event(name="updateComplete", type="org.bookmarksframework.event.BookmarksEvent")]
	
	[Event(name="ready", type="org.bookmarksframework.event.BookmarksEvent")]
	
	[Event(name="register", type="org.bookmarksframework.event.FragmentEvent")]
	
	[Event(name="unregister", type="org.bookmarksframework.event.FragmentEvent")]
	
	[ExcludeClass]
	public class BookmarksManagerImpl extends EventDispatcher implements IBookmarksManager
	{
		protected var map:Map;
		
		protected var config:BookmarksConfig;
		
		protected var storage:IStorage;
		
		protected var committer:ICommitter;
		
		protected var pathQueue:Vector.<PathQueueItem>;
		
		protected var committerFactory:IFactory;
		
		protected var ruleParserFactory:IFactory;
		
		protected var controllerFactory:IFactory;
		
		protected var ready:Boolean = false;
		
		protected var readyFragments:Vector.<IFragment>;
		
		protected var readyCounter:int = -1;
		
		protected var initialized:Boolean = false;
		
		protected var browserAdapter:IBrowserAdapter;
		
		public function initialize(config:BookmarksConfig = null):IBookmarksManager
		{
			if (!config)
				config = new BookmarksConfig();
			
			init(config);
			
			return this;
		}
		
		private var _activePath:Path;
		
		[Bindable("activePathChanged")]
		public function get activePath():Path
		{
			return _activePath;
		}
		
		private var _defaultPath:Path;
		
		[Bindable("defaultPathChanged")]
		public function get defaultPath():Path
		{
			return _defaultPath;
		}
		
		private var viewMapChanged:Boolean = false;
		private var _viewMap:XML;
		
		[Bindable("viewMapChanged")]
		public function get viewMap():XML
		{
			if (!_viewMap || viewMapChanged)
			{
				_viewMap = map.getView();
				viewMapChanged = false;
			}
			
			return _viewMap.copy(); 
		}
		
		public function getFragment(uid:String):IFragment
		{
			var hierarchy:FacileHierarchy = storage.getItem(storage.getKey(uid));
			return (hierarchy) ? IFragmentController(hierarchy.value).fragment : null;
		}
		
		public function register(fragment:IFragment):void
		{
			if (!storage.contains(fragment))
			{
				var c:IFragmentController = controllerFactory.newInstance(); 
				fragment.uid = UIDUtil.createUID().split("-").join("");
				fragment.ruleParser = ruleParserFactory;
				c.fragment = fragment;
				c.addEventListener(FragmentEvent.REGISTER, controllerRegisterHandler);
				c.register();
			}
		}
		
		public function unregister(fragment:IFragment):void
		{
			if (storage.contains(fragment))
			{
				var controllers:Vector.<FacileHierarchy> =
					new Vector.<FacileHierarchy>();
				controllers.push(storage.getItem(storage.getKey(fragment)));
				
				var f:Function = 
					function(controllers:Vector.<FacileHierarchy>):void
				{
					var controller:IFragmentController;
					var children:Vector.<FacileHierarchy>;
					
					for (var i:int = 0; i < controllers.length; ++i)
					{
						controller = controllers[i].value as IFragmentController; 
						if (controller)
						{
							children = controllers[i].getChildren();
							controller.removeEventListener(FragmentEvent.UNREGISTER, 
								controllerUnregisterHandler);
							controller.addEventListener(FragmentEvent.UNREGISTER, 
								controllerUnregisterHandler);
							controller.unregister();
							
							if (children.length)
							{
								arguments.callee.call(null, children);
							}
						}
					}
				}.call(null, controllers);
				
				var controller:IFragmentController = 
					storage.getItem(storage.getKey(fragment)).value as IFragmentController;
				storage.unstore(controller);
			}
		}
		
		public function commit(path:*, fragment:IFragment = null):CommitInfo
		{
			if (!path || !(path is String))
				return null;
			
			path = unescape(path as String);
			
			pathQueue.push(new PathQueueItem(path, fragment, !fragment));
			
			return (committer) ? null : 
				commitPath(pathQueue.shift());
		}
		
		protected function commitPath(value:PathQueueItem):CommitInfo
		{
			var info:CommitInfo = null;
			var path:Path;
			
			try {
				if (value.path)
				{
					path = createPath();
					path.context = value.path;
					path.value = (config.pathResolver != null) ?
						config.pathResolver(value.path) : value.path;
				}
				else if (defaultPath)
				{
					path = defaultPath;
				}
				else
				{
					trace("Bookmarks Error: Path can't be created.");
					return info;
				}
			} catch (e:Error) {
				trace("Bookmarks Error: Path is not well-formed. " +
					"Make sure that context resolver works " +
					"properly if exists or correct path.");
				return info;
			}
			
			path.solid = value.solid;
			path.strict = (config.useStrictPath && !value.fragment);
			committer = ICommitter(committerFactory.newInstance());
			committer.initialize(storage, this);
			committer.delay = config.committmentDelay;
			info = committer.commit(path, value.fragment);
			
			if (info)
			{
				info.addEventListener(Event.COMPLETE, 
					commitCompleteHandler, false, Number.MAX_VALUE);
			}
			
			return info;
		}
		
		protected function update(path:Path = null, fragment:IFragment = null):void
		{
			if (!committer)
			{
				var context:String;
				var currentPath:Path;
				var activePath:Path = this.activePath;
				
				if (!path || !path.solid)
				{
					currentPath = createPath();
					currentPath.strict = config.useStrictPath;
					currentPath.map = map.getActive();
				}
				else
				{
					currentPath = path;
				}
				
				if ((activePath != null) && 
					(activePath.context == currentPath.context))
					return;
				
				if (!currentPath.solid)
				{
					context = (path) ? path.context : 
						((activePath) ? activePath.context : null);
					try {
						currentPath.context = (config.contextFormatter != null) ? 
							config.contextFormatter(currentPath.value, context) :
							currentPath.value;
					} catch (e:Error) {
						trace("Bookmarks Error: Make sure that context " +
							"formatter works properly.");
					}
				}
				
				_activePath = currentPath;
				if (hasEventListener("activePathChanged"))
					dispatchEvent(new Event("activePathChanged"));
				
				viewMapChanged = true;
				if (hasEventListener("viewMapChanged"))
					dispatchEvent(new Event("viewMapChanged"));
				
				if(browserAdapter)
				{
					var browserFragment:String = browserAdapter.fragment;
					context = currentPath.context;
					
					if (currentPath.context && (context != browserFragment))
					{
						if (defaultPath && (currentPath.context == defaultPath.context) && 
							browserAdapter.isFragmentEmpty()) return;
	
						browserAdapter.fragment = currentPath.context;
					}
				}
				
				if (!currentPath.solid)
					updateFragment(fragment);
				
				if (hasEventListener(BookmarksEvent.UPDATE_COMPLETE))
					dispatchEvent(new BookmarksEvent(BookmarksEvent.UPDATE_COMPLETE, currentPath));
			}
		}
		
		protected function updateFragment(fragment:IFragment):void
		{
			if (fragment && fragment.active)
			{
				updateFragmentHelper(storage.getItem(storage.getKey(fragment)));
			}
		}
		
		private function updateFragmentHelper(item:FacileHierarchy):void
		{
			if (item)
			{
				var fragment:IFragment = item.value.fragment;
				fragment.dispatchEvent(
					new FragmentEvent(FragmentEvent.UPDATE));
				var parent:FacileHierarchy = item.getParent(); 
				if (parent) arguments.callee.call(null, parent);
			}
		}
		
		protected function init(config:BookmarksConfig):void
		{
			if (!config.pathSeparator)
				throw new Error("Bookmarks Error: Path separator can't be empty.");
			
			if (this.config && browserAdapter)
			{
				browserAdapter.removeEventListener(
					BrowserAdapterEvents.EXTERNAL_CHANGE, externalURLChangeHandler);
				browserAdapter.destroy();
				browserAdapter = null;
			}
			
			if (config.browserAdapter)
			{
				browserAdapter = config.browserAdapter;
				browserAdapter.addEventListener(BrowserAdapterEvents.EXTERNAL_CHANGE, 
					externalURLChangeHandler);
				browserAdapter.initialize(config);
			}
			
			this.config = config;
			
			if (_defaultPath)
				_defaultPath.strict = config.useStrictPath;
			
			if (!initialized)
			{
				committerFactory = new ClassFactory(Committer);
				controllerFactory = new ClassFactory(FragmentController);
				ruleParserFactory = new ClassFactory(RuleParser);
				
				pathQueue = new Vector.<PathQueueItem>();
				storage = new Storage();
				map = new Map();
				map.initialize(storage);
				readyCounter = -1;
				readyFragments = new Vector.<IFragment>();
				
				initialized = true;
			}
			
		}
		
		protected function externalURLChangeHandler(event:Event):void
		{
			var fragment:String = browserAdapter.fragment;
			
			if (activePath && (activePath.context == fragment))
				return;
			
			fragment = (browserAdapter.isFragmentEmpty()) ? 
				(defaultPath ? defaultPath.value : "") : fragment;
			
			if (fragment)
			{
				pathQueue.push(new PathQueueItem(fragment, null, true));
				
				if(!committer && ready)
				{ 
					commitPath(pathQueue.shift());
				}
			}
		}
		
		protected function controllerRegisterHandler(event:FragmentEvent):void
		{
			var controller:IFragmentController = 
				event.target as IFragmentController;
			controller.removeEventListener(FragmentEvent.REGISTER, 
				controllerRegisterHandler);
			controller.removeEventListener(FragmentEvent.UNREGISTER, 
				controllerUnregisterHandler);
			
			if (storage.store(controller))
			{
				controller.fragment.addEventListener(FragmentChangeEvent.CHANGE, 
					fragmentChangeHandler, false, Number.MAX_VALUE);
				controller.fragment.registered = true;
				
				if (!ready)
				{
					readyCounter++;
					readyFragments.push(controller.fragment);
					CallLater.execute2(readyHelper);
				}
			}
			else
			{
				// error
			}
		}
		
		protected function controllerUnregisterHandler(event:FragmentEvent):void
		{
			var controller:IFragmentController = 
				event.target as IFragmentController;
			controller.removeEventListener(FragmentEvent.UNREGISTER, 
				controllerUnregisterHandler);
			controller.removeEventListener(FragmentEvent.REGISTER, 
				controllerRegisterHandler);
			
			controller.fragment.removeEventListener(FragmentChangeEvent.CHANGE, 
				fragmentChangeHandler);
			controller.fragment.registered = false;
		}
		
		protected function createPath():Path
		{
			var result:Path = new Path();
			result.separator = config.pathSeparator;
			return result;
		}
		
		private function readyHelper():void
		{
			if (!ready && ((--readyCounter) <= 0))
			{
				ready = true;
				
				if (!browserAdapter || 
					(browserAdapter && browserAdapter.isFragmentEmpty()))
				{
					var path:Path = createPath();
					path.map = map.getActive();
					path.context = path.value;
					path.solid = true;
					path.strict = config.useStrictPath;
					_defaultPath = path;
				}
				
				if (hasEventListener("defaultPathChanged"))
					dispatchEvent(new Event("defaultPathChanged"));
				
				if (hasEventListener("viewMapChanged"))
					dispatchEvent(new Event("viewMapChanged"));
				
				if (hasEventListener(BookmarksEvent.READY))
					dispatchEvent(new BookmarksEvent(BookmarksEvent.READY, path));
				
				for (var i:int = 0; i < readyFragments.length; ++i)
				{
					readyFragments[i].dispatchEvent(
						new FragmentEvent(FragmentEvent.UPDATE));
				}
				
				readyFragments = null;
				
				if(!committer && pathQueue.length)
				{ 
					commitPath(pathQueue.shift());
				}
			}
		}
		
		private function fragmentChangeHandler(event:FragmentChangeEvent):void
		{
			if (!committer)
				CallLater.execute2(update, [null, event.fragment]);
		}
		
		private function commitCompleteHandler(event:Event):void
		{
			event.target.removeEventListener(Event.COMPLETE, commitCompleteHandler);
			CallLater.execute2(commitPostComplete);
		}
		
		private function commitPostComplete():void
		{
			var fragments:Vector.<IFragment> = committer.affectedFragments;
			var path:Path = committer.path;;
			var info:CommitInfo = committer.info;
			
			committer = null;
			update(path);
			
			for (var i:int = 0; i < fragments.length; ++ i)
			{
				if (fragments[i].hasEventListener(FragmentEvent.COMMIT_COMPLETE))
					fragments[i].dispatchEvent(new FragmentEvent(
						FragmentEvent.COMMIT_COMPLETE, info.fragment));
			}
			
			if (hasEventListener(BookmarksEvent.COMMIT_COMPLETE))
				dispatchEvent(new BookmarksEvent(
					BookmarksEvent.COMMIT_COMPLETE, activePath));
			
			if (pathQueue.length)
				commitPath(pathQueue.shift());
		}

	}
}

import org.bookmarksframework.core.IFragment;

final class PathQueueItem {
	
	public var path:*;
	
	public var fragment:IFragment;
	
	public var solid:Boolean;
	
	public function PathQueueItem(path:*, fragment:IFragment, solid:Boolean = false)
	{
		this.path = path;
		this.fragment = fragment;
		this.solid = solid;
	}
}